package cn.flkj.evidence;

import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.security.Key;
import java.security.KeyStore;
import java.security.SignatureException;
import java.security.interfaces.ECPrivateKey;
import java.util.*;

import cn.flkj.evidence.web3j.DagTransfer;
import lombok.extern.log4j.Log4j2;
import org.fisco.bcos.channel.client.P12Manager;
import org.fisco.bcos.channel.client.Service;

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;

import cn.flkj.evidence.sample.EvidenceData;
import cn.flkj.evidence.sample.PublicAddressConf;
import cn.flkj.evidence.util.Tools;
import cn.flkj.evidence.web3j.Evidence;
import cn.flkj.evidence.web3j.EvidenceSignersData;
import org.fisco.bcos.web3j.crypto.gm.GenCredential;
import org.fisco.bcos.web3j.tuples.generated.Tuple7;
import org.fisco.bcos.web3j.tx.gas.StaticGasProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.fisco.bcos.web3j.abi.datatypes.Address;
import org.fisco.bcos.web3j.crypto.Credentials;
import org.fisco.bcos.web3j.crypto.ECKeyPair;
import org.fisco.bcos.web3j.crypto.Keys;
import org.fisco.bcos.web3j.crypto.Sign;
import org.fisco.bcos.web3j.protocol.Web3j;
import org.fisco.bcos.web3j.protocol.channel.ChannelEthereumService;
import org.fisco.bcos.web3j.protocol.core.methods.response.TransactionReceipt;
import org.springframework.stereotype.Component;

@Component
@Log4j2
@org.springframework.stereotype.Service
public class EvidenceImpl {
    private final Web3j web3j;
    public static BigInteger gasPrice = new BigInteger("99999999999");
    public static BigInteger gasLimited = new BigInteger("9999999999999");
    private static final String USER = "user.jks";
    private static final String ARBITRATOR = "arbitrator.jks";
    private static final String DEPOSITOR = "depositor.jks";
    private static final String UNIVERSAL_PASSWORD = "123456"; //通用密码，防止写错

   public EvidenceImpl() throws Exception {
        ApplicationContext context = new ClassPathXmlApplicationContext(
                "classpath:applicationContext.xml");
        Service service = context.getBean(Service.class);//获取 service
        service.run(); // 创建一个活跃链接
        ChannelEthereumService channelEthereumService = new ChannelEthereumService();
        channelEthereumService.setChannelService(service);
        // channelEthereumService.setTimeout(10000); 设置交易超时时间为100000毫秒，即100秒
        this.web3j = Web3j.build(channelEthereumService, service.getGroupId()); // 获取Web3j对象
        PublicAddressConf conf = context.getBean(PublicAddressConf.class);
        ConcurrentHashMap<String, String> addressConf = conf.getAllPublicAddress();
        log.info("检验XML数据是否为空 : " + addressConf.isEmpty()); // 检验XML数据是否加载
        P12Manager p12 = context.getBean(P12Manager.class);// 加载账户，获取公钥和私钥
        ECKeyPair p12KeyPair = p12.getECKeyPair();// 提供密码获取ECKeyPair，密码在生产p12账户文件时指定
        log.info("p12 privateKey: " + p12KeyPair.getPrivateKey().toString(16));// 以十六进制串输出私钥和公钥
        log.info("p12 publicKey: " + p12KeyPair.getPublicKey().toString(16));
        Credentials credentials = GenCredential.create(p12KeyPair.getPrivateKey().toString(16));// 生成Web3SDK使用的Credentials
        log.info("p12 Address: " + credentials.getAddress());
        BigInteger blockNumber = web3j.getBlockNumber().send().getBlockNumber();// 通过Web3j对象调用API接口getBlockNumber
        log.info(blockNumber.toString()); // call HelloWorldPrecompiled  0x5001 get
        DagTransfer contract = DagTransfer.load(
                "0x0000000000000000000000000000000000005002", web3j, credentials,
                new StaticGasProvider(gasPrice, gasLimited));
        log.info("bilibili balance: " + contract.userBalance("bilibili").send());
    }


    /**
     * 生成新存证合约 && 二次签名，
     *
     * @throws Exception 密码 地址 等相关异常
     */
    public void newEvidenceAddressAndSendSignatureToBlockChain() throws Exception {
        String newEvidenceAddress = newEvidence(
                USER,
                "123456",
                "123456",
                "0x0f74e2529e561b2749034a3d235da8cf04ab8bea",
                "1",
                "85de67c115fc6fe46d5c935569fea35a").getValue();
        // 0xf55b09f836195382cb3e254d5d5be63f9eae6110
        log.info("newEvidenceAddress : " + newEvidenceAddress);
        EvidenceData evidenceData = getEvidence(
                USER,
                "123456",
                "123456",
                newEvidenceAddress);
        String evidenceHash = evidenceData.getEvidenceHash();
        // 85de67c115fc6fe46d5c935569fea35a
        log.info("getEvidenceHash : " + evidenceHash);
        boolean falg = sendSignatureToBlockChain(
                ARBITRATOR,
                "123456",
                "123456",
                newEvidenceAddress,
                evidenceHash
        );
        if (falg) {
            System.out.println("-----------sendSignatureToBlockChain success！" + falg);
        } else {
            System.out.println("------------sendSignatureToBlockChain failed！" + falg);
        }
        //String newEvidenceAddress = "0xf55b09f836195382cb3e254d5d5be63f9eae6110";
        //String evidenceHash = "85de67c115fc6fe46d5c935569fea35a";
        boolean falg1 = sendSignatureToBlockChain(
                DEPOSITOR,
                UNIVERSAL_PASSWORD,
                UNIVERSAL_PASSWORD,
                newEvidenceAddress,
                evidenceHash
        );
        if (falg1) {
            System.out.println("-----------sendSignatureToBlockChain success！" + falg1);
        } else {
            System.out.println("------------sendSignatureToBlockChain failed！" + falg1);
        }
    }

    /**
     * 用户创建包含存证摘要的存证合约，返回新存证的地址
     *
     * @param keyStoreFileName       私钥文件名 user.jks
     * @param keyStorePassword       123456
     * @param keyPassword            123456
     * @param evidenceSignersAddress 工厂合约地址，0x0f74e2529e561b2749034a3d235da8cf04ab8bea
     * @param evidenceId             证据ID，所以随意填写,建议从1开始
     * @param evidenceHash           存证的hash，长度没有限制,85de67c115fc6fe46d5c935569fea35a
     * @return 返回包含存证摘要的存证合约的地址
     * @throws Exception 创建存证失败的异常
     */
    public Address newEvidence(
            String keyStoreFileName, // 私钥文件名 user.jks
            String keyStorePassword, // 123456
            String keyPassword, // 123456
            String evidenceSignersAddress, // 工厂合约地址，0x0f74e2529e561b2749034a3d235da8cf04ab8bea
            String evidenceId, // 证据ID，所以随意填写
            String evidenceHash // 存证的hash，长度没有限制,85de67c115fc6fe46d5c935569fea35a
    ) throws Exception {
        Credentials credentials = loadKey(keyStoreFileName, keyStorePassword, keyPassword); // 获取证书
        EvidenceSignersData evidenceSignersData = EvidenceSignersData.load(evidenceSignersAddress, web3j, credentials,
                new StaticGasProvider(gasPrice, gasLimited)); // 通过已经部署的地址加载工厂智能合约
        Sign.SignatureData data = Sign.getSignInterface().signMessage(
                evidenceHash.getBytes(), credentials.getEcKeyPair());// 通过用户私钥匙对存证进行签名
        String signData = Tools.signatureDataToString(data);
        Sign.SignatureData signatureData = Tools.stringToSignatureData(signData);
        TransactionReceipt receipt = evidenceSignersData.newEvidence(evidenceHash, evidenceId, evidenceId,
                BigInteger.valueOf(signatureData.getV()), signatureData.getR(), signatureData.getS())
                .sendAsync().get(); // 创建证据返回的交易凭证，存证合约已经包含存证的摘要
        List<EvidenceSignersData.NewEvidenceEventEventResponse> newEvidenceList =
                evidenceSignersData.getNewEvidenceEventEvents(receipt);
        return new Address(newEvidenceList.get(0).addr);
    }

    /*
    sendSignatureToBlockChain 发送签名到区块链子
    注意：此命令需要执行两次，在newEvidence步骤时，用户已经签名，在这个步骤需要存证机构和仲裁机构双方签名，
    所以在执行这个命令时传入的keyStoreFileName、keyStorePassword、keyPassword分别为两个机构的私钥及
    密码。在这个步骤中同样需要用到evidence_hash值，程序中通过newEvidenceAddress查询证据的信息，然后获
    取evidence_hash值。命令中第一个参数不可修改，newEvidenceAddress为创建证据区块链返回的地址。
    1.私钥文件名 2.keyStorePassword 3.keyPassword 4.newEvidenceAddress 5.evidence_hash
     */

    /**
     * 发送签名到区块链,分别需要存证机构和仲裁机构双方签名
     *
     * @param keyStoreFileName   私钥文件名
     * @param keyStorePassword   123456
     * @param keyPassword        123456
     * @param newEvidenceAddress 包含存证摘要的存证合约的地址
     * @param evidenceHash       存证的Hash 85de67c115fc6fe46d5c935569fea35a
     * @return 是否成功
     */
    public boolean sendSignatureToBlockChain(
            String keyStoreFileName,
            String keyStorePassword,
            String keyPassword,
            String newEvidenceAddress, //存证智能合约地址
            String evidenceHash
    ) throws InterruptedException, ExecutionException, SignatureException {
        Credentials credentials = loadKey(keyStoreFileName, keyStorePassword, keyPassword);
        Evidence evidence = Evidence.load(newEvidenceAddress, web3j, credentials,
                new StaticGasProvider(gasPrice, gasLimited));
        Sign.SignatureData data = Sign.getSignInterface().signMessage(
                evidenceHash.getBytes(), credentials.getEcKeyPair()); //使用公私钥对数据进行签名
        boolean flag = false;
        String signatureString = Tools.signatureDataToString(data);
        Sign.SignatureData signature = Tools.stringToSignatureData(signatureString);
        String recoverAddress = verifySignedMessage(evidenceHash, signatureString);//验证签名消息
        if (!credentials.getAddress().equals(recoverAddress)) throw new SignatureException();
        TransactionReceipt receipt = evidence.addSignatures(
                BigInteger.valueOf(signature.getV()),
                signature.getR(),
                signature.getS()).sendAsync().get(); // 创建证据返回的交易凭证
        List<Evidence.AddSignaturesEventEventResponse> addList =
                evidence.getAddSignaturesEventEvents(receipt);
        List<Evidence.AddRepeatSignaturesEventEventResponse> addList2 =
                evidence.getAddRepeatSignaturesEventEvents(receipt);
        if (addList.size() > 0 || addList2.size() > 0) flag = true;
        return flag;
    }

    /**
     * 验证签名消息
     *
     * @param message       原始消息
     * @param signatureData 签名后的消息
     * @return recoverAddress
     * @throws SignatureException 调用需要处理异常
     */
    public String verifySignedMessage(String message, String signatureData)
            throws SignatureException {
        Sign.SignatureData signatureData1 = Tools.stringToSignatureData(signatureData);
        return "0x" + Keys.getAddress(
                Sign.signedMessageToKey(message.getBytes(), signatureData1));
    }

    /**
     * 通过证据地址获取证据信息
     *
     * @param keyStoreFileName .jks文件
     * @param keyStorePassword 123456
     * @param keyPassword      123456
     * @param transactionHash  newEvidenceAddress,合约地址
     * @return EvidenceData 类实例
     */
    public EvidenceData getEvidence(
            String keyStoreFileName,
            String keyStorePassword,
            String keyPassword,
            String transactionHash
    ) throws Exception {
        Credentials credentials = loadKey(keyStoreFileName, keyStorePassword, keyPassword);
        Evidence evidence = Evidence.load(transactionHash, web3j, credentials,
                new StaticGasProvider(gasPrice, gasLimited)); //通过指定地址加载存证合约
        EvidenceData evidenceData = new EvidenceData();
        Tuple7<String,
                String,
                String,
                List<BigInteger>,
                List<byte[]>,
                List<byte[]>,
                List<String>> result2 = evidence.getEvidence().send();
        if (result2 == null) return null;//证据字段为6个
        evidenceData.setEvidenceHash(result2.getValue1());
        evidenceData.setEvidenceInfo(result2.getValue2());
        evidenceData.setEvidenceID(result2.getValue3());
        List<BigInteger> vlist = result2.getValue4();
        List<byte[]> rlist = result2.getValue5();
        List<byte[]> slist = result2.getValue6();
        ArrayList<String> signatureList = new ArrayList<String>();
        for (int i = 0; i < vlist.size(); i++) {
            Sign.SignatureData signature = new Sign.SignatureData(
                    vlist.get(i).byteValue(), rlist.get(i), slist.get(i));
            signatureList.add(Tools.signatureDataToString(signature));
        }
        evidenceData.setSignatures(signatureList);
        List<String> addresses = result2.getValue7();
        ArrayList<String> addressesList = new ArrayList<String>(addresses);
        addressesList.addAll(addresses);
        evidenceData.setPublicKeys(addressesList);
        return evidenceData;
    }

    /**
     * 公钥私钥匙获取证书
     *
     * @param keyStoreFileName .jks文件
     * @param keyStorePassword 123456
     * @param keyPassword      123456
     * @return 证书
     */
    public Credentials loadKey(String keyStoreFileName, String keyStorePassword, String keyPassword) {
        InputStream ksInputStream = null;
        try {
            KeyStore ks = KeyStore.getInstance("JKS");
            ksInputStream = Runner.class.getClassLoader().getResourceAsStream(keyStoreFileName);
            ks.load(ksInputStream, keyStorePassword.toCharArray());
            Key key = ks.getKey("ec", keyPassword.toCharArray());
            ECKeyPair keyPair = ECKeyPair.create(((ECPrivateKey) key).getS());
            return Credentials.create(keyPair);
        } catch (Exception e) {
            e.printStackTrace();
            log.info("读取jks失败 ： ", e);
        } finally {
            try {
                if (null != ksInputStream) {
                    ksInputStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return null;
    }

    /**
     * 通过私钥文件 .jks 获取公钥
     *
     * @param keyStoreFileName .jks
     * @param keyStorePassword 123456
     * @param keyPassword      123456
     * @return 公钥
     */
    public String getPublicKey(String keyStoreFileName, String keyStorePassword, String keyPassword) {
        Credentials credentials = loadKey(keyStoreFileName, keyStorePassword, keyPassword);
        return credentials.getAddress();
    }

    /**
     * 核实证据
     *
     * @param data 合约模板
     * @return 检验结果
     * @throws SignatureException 签名异常
     */
    public boolean verifyEvidence(EvidenceData data) throws SignatureException {
        ArrayList<String> addressList = new ArrayList<>();
        for (String str : data.getSignatures()) {
            try {
                addressList.add(verifySignedMessage(data.getEvidenceHash(), str));
            } catch (SignatureException e) {
                throw e;
            }
        }
        for (String addr : data.getPublicKeys()) {
            boolean flag = false;
            for (String str : addressList) {
                if (str.equals(addr)) {
                    flag = true;
                    break;
                }
            }
            if (!flag) {
                return false;
            }
        }
        return true;
    }

    /**
     * experience 体验
     *
     * @throws Exception 获取存证可能异常
     */
    public void experience() throws Exception {
        System.out.println("-------------------------- WEB3J INIT OK !!! --------------------------");
        EvidenceData evidenceData1 = getEvidence(
                USER, UNIVERSAL_PASSWORD, UNIVERSAL_PASSWORD,
                "0xf55b09f836195382cb3e254d5d5be63f9eae6110");
        boolean flag = verifyEvidence(evidenceData1);
        if (flag) {
            System.out.println("--------verifyEvidence success:" + flag);
        } else {
            System.out.println("--------verifyEvidence failed:" + flag);
        }
    }

}
